﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
// Krzysztof Heim
// pktools.codeplex.com

namespace PKTools.Serialize
{
    public class FastSerializer
    {
        const int
            maxDepth = 16,  // max nesting level
            maxInlineTypes = 24; // max number of inline types
        byte
            index;

        Dictionary<Type, KeyValuePair<byte, Action<BinaryWriter, object>>>
            encoders = new Dictionary<Type, KeyValuePair<byte, Action<BinaryWriter, object>>>();
        Dictionary<byte, KeyValuePair<Type, Func<BinaryReader, object>>>
            decoders = new Dictionary<byte, KeyValuePair<Type, Func<BinaryReader, object>>>();

        protected void AddType(Type type, Action<BinaryWriter, object> encode, Func<BinaryReader, object> decode)
        {
            this.encoders.Add(type, new KeyValuePair<byte, Action<BinaryWriter, object>>(this.index, encode));
            this.decoders.Add(this.index, new KeyValuePair<Type, Func<BinaryReader, object>>(type, decode));
            ++this.index;
        }

        public FastSerializer()
        {
            // null
            this.decoders.Add(0, new KeyValuePair<Type, Func<BinaryReader, object>>(typeof(object), r => null));

            this.index = 1;
            // inline types            
            AddType(typeof(Int16), (w, v) => w.Write((Int16)v), r => r.ReadInt16());
            AddType(typeof(Int32), (w, v) => w.Write((Int32)v), r => r.ReadInt32());
            AddType(typeof(Int64), (w, v) => w.Write((Int64)v), r => r.ReadInt64());
            AddType(typeof(UInt16), (w, v) => w.Write((UInt16)v), r => r.ReadUInt16());
            AddType(typeof(UInt32), (w, v) => w.Write((UInt32)v), r => r.ReadUInt32());
            AddType(typeof(UInt64), (w, v) => w.Write((UInt64)v), r => r.ReadUInt64());
            AddType(typeof(Decimal), (w, v) => w.Write((Decimal)v), r => r.ReadDecimal());
            AddType(typeof(Double), (w, v) => w.Write((Double)v), r => r.ReadDouble());
            AddType(typeof(Boolean), (w, v) => w.Write((Boolean)v), r => r.ReadBoolean());
            AddType(typeof(Byte), (w, v) => w.Write((Byte)v), r => r.ReadByte());
            AddType(typeof(Char), (w, v) => w.Write((Char)v), r => r.ReadChar());
            AddType(typeof(String), (w, v) => w.Write(v as String), r => r.ReadString());
            AddType(typeof(TimeSpan), (w, v) => w.Write(((TimeSpan)v).Ticks), r => new TimeSpan(r.ReadInt64()));
            AddType(typeof(DateTime), (w, v) => w.Write(((DateTime)v).Ticks), r => new DateTime(r.ReadInt64()));
            AddType(typeof(Guid), (w, v) => w.Write(((Guid)v).ToByteArray()), r => new Guid(r.ReadBytes(16)));
        }

        protected virtual void AddUserTypes()
        {
            // override this method to add more inline types
        }

        protected virtual IEnumerable<FieldInfo> GetFields(Type type)
        {
            // override this method to change which objects fields should be serialized
            return type.
                GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).
                Where(e => !e.IsPrivate && !e.IsNotSerialized && !e.IsAssembly);
        }

        public void Encode(BinaryWriter writer, object graph, int depth)
        {
            if (depth == maxDepth)
                throw new Exception("max. object depth");

            if (graph == null)
            {
                writer.Write((byte)0);  // null object to output
                return;
            }

            var
                type = graph.GetType();

            KeyValuePair<byte, Action<BinaryWriter, object>>
                encoder;

            if (!this.encoders.TryGetValue(type, out encoder))    // check if encoder already exists
            {
#if DEBUG
                if (this.index == 255)
                    throw new Exception("too many types");
#endif
                // encoder for this type don't exist, create one
                encoder = new KeyValuePair<byte, Action<BinaryWriter, object>>(this.index++, null);
                this.encoders.Add(type, encoder);

                writer.Write(encoder.Key);  // emit encoder definition to output
                writer.Write(type.AssemblyQualifiedName);
            }
            else
                writer.Write(encoder.Key);  // encoder exists, so emit it's ID

            if (encoder.Value != null)
            {
                encoder.Value(writer, graph);   // direct conversion exist
                return;
            }

            if (type.IsEnum)
            {                
                // emit enum
                Encode(writer, Convert.ChangeType(graph, Enum.GetUnderlyingType(type)), depth + 1);
                return;
            }

#if DEBUG
            if (type.IsValueType)
                throw new Exception("complex type expected: " + type.FullName);
#endif

            if (type.IsArray)
            {
                // emit array
                writer.Write((graph as Array).Length);
                foreach (var e in graph as Array)
                    Encode(writer, e, depth + 1);

                return; // array can't be derived so no fields
            }

            if (graph is IList)
            {
                // emit generic list
                writer.Write((graph as IList).Count);
                foreach (var e in graph as IList)
                    Encode(writer, e, depth + 1);
            }
            else
            if (graph is IDictionary)
            {
                // emit dictionary
                writer.Write((graph as IDictionary).Count);

                foreach (DictionaryEntry e in graph as IDictionary)
                {
                    Encode(writer, e.Key, depth + 1);
                    Encode(writer, e.Value, depth + 1);
                }
            }                      

            // object fields
            foreach (var field in GetFields(type))
                Encode(writer, field.GetValue(graph), depth + 1);
        }

        KeyValuePair<Type, Func<BinaryReader, object>> ReadType(BinaryReader reader)
        {
            byte
                typeID = reader.ReadByte();

            KeyValuePair<Type, Func<BinaryReader, object>>
                decoder;

            if (!this.decoders.TryGetValue(typeID, out decoder))
            {
                decoder = new KeyValuePair<Type, Func<BinaryReader, object>>(Type.GetType(reader.ReadString(), true), null);
                this.decoders.Add(typeID, decoder);
            }

            return decoder;
        }

        object Decode(BinaryReader reader)
        {
            var
                decoder = ReadType(reader); // read type

            if (decoder.Value != null)  // direct decoder exists
                return decoder.Value(reader);

            var
                type = decoder.Key;

            if (type.IsEnum)    // enum
                return Enum.ToObject(type, Decode(reader));

            int
                count;

            if (type.IsArray)   // array
            {
                count = reader.ReadInt32();
                var
                    array = Array.CreateInstance(type.GetElementType(), count);

                for (int i = 0; i < count; i++)
                    array.SetValue(Decode(reader), i);

                return array;
            }

            object
                graph = Activator.CreateInstance(type);

            if (graph is IList) // list
            {
                var
                    list = graph as IList;

                count = reader.ReadInt32();

                while (count-- > 0)
                    list.Add(Decode(reader));
            }
            else
            if (graph is IDictionary)   // dictionary
            {
                var
                    dict = graph as IDictionary;

                count = reader.ReadInt32();

                while (count-- > 0)
                    dict.Add(Decode(reader), Decode(reader));
            }

            // object fields
            foreach (var field in GetFields(type))
                field.SetValue(graph, Decode(reader));            

            return graph;
        }

        void ClearUserTypes()
        {
            // copy inline types only
            this.encoders = this.encoders.Take(maxInlineTypes).ToDictionary(e => e.Key, e => e.Value);
            this.decoders = this.decoders.Take(maxInlineTypes).ToDictionary(e => e.Key, e => e.Value);

            this.index = maxInlineTypes;    // custom types start

            AddUserTypes();
        }

        public void Serialize(Stream stream, object graph)
        {
            ClearUserTypes();

            using (var writer = new BinaryWriter(stream))
                Encode(writer, graph, 0);
        }

        public byte[] ToBytes(object graph)
        {
            using (var m = new MemoryStream())
            {
                Serialize(m, graph);
                return m.ToArray();
            }
        }

        public object Deserialize(Stream stream)
        {
            ClearUserTypes();

            using (var reader = new BinaryReader(stream))
                return Decode(reader);
        }

        public object FromBytes(byte[] data)
        {
            using (var m = new MemoryStream(data))
                return Deserialize(m);
        }
    }
}
